<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <meta charset="utf-8">
  <title>访问者流</title>
  <style>
    @import url(css/style.css);
    #chart {
      height: 500px;
    }

    .node rect {
      cursor: move;
      fill-opacity: .9;
      shape-rendering: crispEdges;
    }

    .node text {
      pointer-events: none;
      text-shadow: 0 1px 0 #fff;
    }

    .link {
      /*
      fill: none;
      */
      fill: #33CCFF;
      fill-opacity: .3;
      stroke: #33CCFF;
      /*
      stroke-opacity: .2;
      */
      stroke-opacity: .6;
    }

    .link:hover {
      fill-opacity: .6;
    }

    .fglink {
      fill: #33CCFF;
      fill-opacity: .3;
      stroke: #33CCFF;
      stroke-opacity: .6;
    }

    .fglink:hover {
      fill-opacity: .6;
    }

    .rect {
      overflow: hidden;
    }

    #netFlowHead {
      position: relative;
      padding: 5px 0 5px 0;
      height: 90px;
    }

    .levelSummary {
      background-color: #ddf;
    }

    .bgsvg {
      position: absolute;
      left: 0;
      top: 0;
    }

    .fgsvg {
      position: absolute;
      left: 0;
      top: 0;
    }
  </style>
</head>

<body>
  <div id="netFlow">
    <div id="netFlowHead"></div>
    <div id="chart"></div>
  </div>

  <script src="js/d3.js"></script>
  <script src="js/netflow.js"></script>
  <script src="js/jquery-1.7.1.js"></script>
  <script>
    //config
    var nodeWidth = 100; //节点宽度
    var linkHLWidth = 10; //linkHorizonalLineWidth
    var nodePadding = 1.5 * linkHLWidth; //节点上下间距

    var margin = {
        top: 10,
        right: 20,
        bottom: 20,
        left: 10
      }, //间距
      width = 960 - margin.left - margin.right, //宽
      height = 500 - margin.top - margin.bottom; //高

    //data
    var data = {
      nodeIndex: {}, // key is level + '_' + name
      linkIndex: {}, // key is sourceIndexInArray + '_' + targetIndexInArray
      nodes: undefined,
      links: undefined,
      rects: [] //jqnodes array
    };

    //state
    var state = 'all'; // 'all', 'part'

    //global
    var floatTag, //浮框jquery对象
      fgsvg, //前景svg d3对象
      bgsvg, //背景svg d3对象
      sankey; //sankey布局对象

    //init
    var init = function() {
      //init chart;
      $("#chart").css("position", "relative");
      floatTag = createFloatTag()($("#chart"));
      floatTag.css({
        "visibility": "hidden"
      });

      //init svg
      fgsvg = d3.select("#chart").append("svg")
        .attr("class", "fgsvg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .style("z-index", 100)
        .style("visibility", "hidden");
      fgsvg.append("rect")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .style("fill", "#fff")
        .style("fill-opacity", 0.7);
      fgsvg = fgsvg.append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

      bgsvg = d3.select("#chart").append("svg")
        .attr("class", "bgsvg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
      var lostGradient = bgsvg.append("defs").append("linearGradient")
        .attr("id", "lostGradient")
        .attr("x1", "100%")
        .attr("y1", "100%")
        .attr("x2", "100%")
        .attr("y2", "0%");
      lostGradient.append("stop")
        .attr("stop-color", "white")
        .attr("offset", "0%");
      lostGradient.append("stop")
        .attr("stop-color", "red")
        .attr("offset", "100%");
      var nodeGradient = bgsvg.append("defs").append("linearGradient")
        .attr("id", "nodeGradient")
        .attr("x1", "100%")
        .attr("y1", "100%")
        .attr("x2", "100%")
        .attr("y2", "0%");
      nodeGradient.append("stop")
        .attr("stop-color", "rgb(81,219,122)")
        .attr("offset", "0%");
      nodeGradient.append("stop")
        .attr("stop-color", "rgb(202,237,204)")
        .attr("offset", "65%");

      //init layout
      sankey = d3.sankey()
        //.nodeWidth(15)
        .nodeWidth(nodeWidth)
        .nodePadding(nodePadding)
        .size([width, height]);
    };

    //link path creator
    var createLinkPath = function(state) { //all, part
      return function(d) {
        var curvature = .5;
        var horizontalLineWidth = linkHLWidth;
        var ratio;
        if (state === "all") {
          ratio = 1;
        } else {
          ratio = d.value / d.originLink.value;
          d = d.originLink;
        }
        var x0 = d.source.x + d.source.dx,
          xline0 = x0 + horizontalLineWidth,
          x1 = d.target.x,
          xline1 = x1 - horizontalLineWidth,
          xi = d3.interpolateNumber(x0, x1),
          x2 = xi(curvature),
          x3 = xi(1 - curvature),
          /*
           y0top = d.source.y + d.sy,
           y1top = d.target.y + d.ty,
           y0down = d.source.y + d.sy + d.dy,
           y1down = d.target.y + d.ty + d.dy;
           */
          y0top = d.source.y + d.sy + (1 - ratio) / 2 * d.dy,
          y1top = d.target.y + d.ty + (1 - ratio) / 2 * d.dy,
          y0down = d.source.y + d.sy + d.dy - (1 - ratio) / 2 * d.dy,
          y1down = d.target.y + d.ty + d.dy - (1 - ratio) / 2 * d.dy;
        return "M" + x0 + "," + y0top + "L" + xline0 + "," + y0top + "C" + x2 + "," + y0top + " " + x3 + "," + y1top + " " + xline1 + "," + y1top + "L" + x1 + "," + y1top + "L" + x1 + "," + y1down + "L" + xline1 + "," + y1down + "C" + x3 + "," +
          y1down + " " + x2 + "," + y0down + " " + xline0 + "," + y0down + "L" + x0 + "," + y0down + "Z";
      };
    };

    //create lost path
    var createLostPath = function(state) { //"part", "all"
      return function(d) {
        var y0, ratio;
        var x = sankey.nodeWidth();
        var width = linkHLWidth;
        var y, height;
        if (state === "all") {
          y = d.dy * d.output / d.value;
          height = d.dy - y;
          if (d.value - d.output === 0) {
            return "";
          }
          y0 = y;
        } else {
          y = d.originNode.dy * d.originNode.output / d.originNode.value;
          height = d.originNode.dy - y;
          if (d.value - d.output === 0 || d.originNode.value - d.originNode.output === 0) {
            return "";
          }
          ratio = (d.value - d.output) / (d.originNode.value - d.originNode.output);
          y0 = y + height * (1 - ratio);
        }
        return "M" + x + "," + y0 + "C" + x + "," + y0 + " " + (x + width) + "," + y0 + " " + (x + width) + "," + (y0 + width) + "L" + (x + width) + "," + (y + width + height) + "L" + x + "," + (y + width + height) + "Z";
      };
    }

    //level summary config
    var createHead = function(nodes) {
      var head = $("#netFlowHead");
      var nodesByLevel = d3.nest()
        .key(function(d) {
          return d.x;
        })
        .sortKeys(d3.ascending)
        .entries(nodes)
        .map(function(d) {
          return d.values;
        });
      nodesByLevel.forEach(function(d) {
        var node = d[0];
        if (node.level === 0) {
          return;
        }
        var input = d3.sum(d, function(d) {
          return d.input
        });
        var output = d3.sum(d, function(d) {
          return d.output
        });
        var getContent = function(node, input, output) {
          var getLevelName = function(level) {
            if (level === 1) {
              return "起始网页";
            } else {
              return "第" + (level - 1) + "次互动";
            }
          };
          return "<p>" + getLevelName(node.level) + "</p><p>" + output + " / " + input + "</p>";
        };
        $("<div/>").addClass("levelSummary")
          .css({
            "position": "absolute",
            "left": margin.left + node.x - 2 + "px", // -2, -1 is small adjust
            "width": sankey.nodeWidth() + 1 + "px"
              //"top": margin.top + "px",
              //"height": 0 + "px"
          })
          .html(getContent(node, input, output))
          .appendTo(head);
      });
    };

    //draw background svg
    var createSVG = function(flowData) {
      //link
      var link = bgsvg.append("g").selectAll(".link")
        .data(flowData.links)
        .enter().append("path")
        .attr("class", "link")
        .attr("d", createLinkPath("all"))
        .on("click", allClickListener);
      link.append("title")
        .text(function(d) {
          return d.source.name + " → " + d.target.name + "\n" + d.value;
        });

      //node container
      var node = bgsvg.append("g").selectAll(".node")
        .data(flowData.nodes)
        .enter().append("g")
        .attr("class", "node")
        .attr("transform", function(d) {
          return "translate(" + d.x + "," + d.y + ")";
        });

      //node
      node.append("rect")
        .attr("height", function(d) {
          return d.dy;
        })
        .attr("width", sankey.nodeWidth())
        .style("fill", function(d) {
          return d.color = d.level === 0 ? "white" : "url(#nodeGradient)";
        })
        .style("stroke", function(d) {
          return d3.rgb(d.color).darker(2);
        });

      //lost path
      node.append("path")
        .attr("d", createLostPath("all"))
        .style("fill", function(d) {
          return "url(#lostGradient)";
        });
      //.style("fill", function(d) { return "red"; })
    };

    //create jquery rect nodes
    var createJqNodes = function(flowData) {
      var createRect = function(node) {
        var rect = $("<div/>")
          .addClass("rect")
          //.addClass(node.level === 0 ? "" : "gradientRect")
          .css({
            "position": "absolute",
            "left": margin.left + node.x + "px", // -2, -1 is small adjust
            "top": margin.top + node.y + "px",
            "width": sankey.nodeWidth() + "px",
            //"height": node.dy + "px",
            //"background-color": "white",
            "z-index": 10
          })
          .appendTo($("#chart"));
        rect.html("<p>" + node.name + "</p>" + "<p>" + node.value + "</p>");
        if (rect.height() < node.dy) {
          //让节点div能够覆盖节点svg
          rect.height(node.dy + "px");
        } else if (rect.height() > node.dy + nodePadding) {
          //节点不能覆盖下一个节点
          rect.height(node.dy + nodePadding + "px");
        }
        return rect;
      };

      flowData.nodes.forEach(function(d) {
        var rect = createRect(d);
        //bind data to rect
        rect[0].nodeInfo = d;
        d.rect = rect;
        //data
        data.nodeIndex[d.level + '_' + d.name] = d;
        data.rects.push(rect);
      });
    };

    //create float tag
    var createFloatTag = function() {
      var _mousemove = function(e) {
        var jqNode = e.data.jqNode;
        var container = e.data.container;
        var mouseToFloatTag = {
          x: 20,
          y: 20
        };
        var offset = $(container).offset();
        if (!(e.pageX && e.pageY)) {
          return false;
        }
        var x = e.pageX - offset.left,
          y = e.pageY - offset.top;
        var position = $(container).position();

        setContent.call(this);

        //set floatTag location
        floatTagWidth = jqNode.outerWidth();
        floatTagHeight = jqNode.outerHeight();
        if (floatTagWidth + x + 2 * mouseToFloatTag.x <= $(container).width()) {
          x += mouseToFloatTag.x;
        } else {
          x = x - floatTagWidth - mouseToFloatTag.x;
        }
        if (y >= floatTagHeight + mouseToFloatTag.y) {
          y = y - mouseToFloatTag.y - floatTagHeight;
        } else {
          y += mouseToFloatTag.y;
        }
        jqNode.css("left", x + "px");
        jqNode.css("top", y + "px");
      };

      var setContent = function() {};

      function floatTag(cont) {
        var container = cont;
        var jqNode = $("<div/>").css({
          "border": "1px solid",
          "border-color": $.browser.msie ? "rgb(0, 0, 0)" : "rgba(0, 0, 0, 0.8)",
          "background-color": $.browser.msie ? "rgb(0, 0, 0)" : "rgba(0, 0, 0, 0.75)",
          "color": "white",
          "border-radius": "2px",
          "padding": "12px 8px",
          //"line-height": "170%",
          //"opacity": 0.7,
          "font-size": "12px",
          "box-shadow": "3px 3px 6px 0px rgba(0,0,0,0.58)",
          "font-familiy": "宋体",
          "z-index": 10000,
          "text-align": "center",
          "visibility": "hidden",
          "position": "absolute"
        });
        $(container).append(jqNode)
          .mousemove({
            "jqNode": jqNode,
            "container": container
          }, _mousemove);
        return jqNode;
      }

      floatTag.setContent = function(sc) {
        if (arguments.length === 0) {
          return setContent;
        }
        setContent = sc;
      };
      return floatTag;
    };

    //show part flow data
    var allToPart = function(flowData) {
      state = "part";
      floatTag.css("visibility", "hidden");

      var sankeyPart = d3.sankey()
        .nodeWidth(nodeWidth)
        .nodePadding(nodePadding)
        .size([width, height])
        .nodes(flowData.nodes)
        .links(flowData.links)
        .layout(0);

      //get origin node
      flowData.nodes.forEach(function(d) {
        //find related nodes
        d.originNode = data.nodeIndex[d.level + '_' + d.name];
      });
      //get origin link
      flowData.links.forEach(function(d) {
        //find related link
        var originSource = d.source.originNode;
        var originTarget = d.target.originNode;
        d.originLink = data.linkIndex[originSource.indexInArray + "_" + originTarget.indexInArray];
      });

      //show fg
      $("svg.fgsvg").css("visibility", "visible");

      //draw link
      var link = fgsvg.append("g").selectAll(".link")
        .data(flowData.links)
        .enter().append("path")
        .attr("class", "fglink")
        .attr("d", createLinkPath("part"));

      link.append("title")
        .text(function(d) {
          return d.source.name + " → " + d.target.name + "\n" + d.value;
        });

      //draw node
      var node = fgsvg.append("g").selectAll(".node")
        .data(flowData.nodes)
        .enter().append("g")
        .attr("class", "node")
        .attr("transform", function(d) {
          return "translate(" + d.originNode.x + "," + d.originNode.y + ")";
        });

      node.append("rect")
        .attr("height", function(d) {
          return d.originNode.dy;
        })
        .attr("width", sankey.nodeWidth())
        //.style("fill", function(d) { return d.color = color(d.name.replace(/ .*/, "")); })
        .style("fill", function(d) {
          return d.originNode.color = d.originNode.level === 0 ? "white" : "url(#nodeGradient)";
        })
        .style("stroke", function(d) {
          return d3.rgb(d.originNode.color).darker(2);
        });

      //draw lost percentage
      node.append("path")
        .attr("d", createLostPath("part"))
        .style("fill", function(d) {
          return "url(#lostGradient)";
        });

      //refresh all rect
      flowData.nodes.forEach(function(node) {
        node.originNode.rect
          .css({
            "z-index": 1000
          })
          .html("<p>" + node.name + "</p>" + "<p>" + node.value + " / " + node.originNode.value + "</p>");
        node.originNode.rect[0].nodeInfo = node;
      });
      data.rects.forEach(function(d) {
        if (parseInt(d.css("z-index"), 10) === 10) {
          node = d[0].nodeInfo;
          d.html("<p>" + node.name + "</p>" + "<p>" + 0 + " / " + node.value + "</p>");
        }
      });
      //bind event
    };

    //stop show part flow data, show all data
    var partToAll = function() {
      state = "all";
      floatTag.css("visibility", "hidden");
      //clear and show fg
      fgsvg.selectAll("g").remove();
      $("svg.fgsvg").css("visibility", "hidden");

      //refresh rect
      data.rects.forEach(function(d) {
        node = d[0].nodeInfo;
        d.css("z-index", 10)
          .html("<p>" + node.name + "</p>" + "<p>" + node.value + "</p>");
        if (typeof d[0].nodeInfo.originNode !== 'undefined') {
          d[0].nodeInfo = d[0].nodeInfo.originNode;
        }
      });
    };

    //click Listener in "all" state
    var allClickListener = function(e) {
      if (state === "part") {
        return;
      }
      var event;
      var url;
      if (e.type === 'click') { //jquery event
        event = e;
        url = 'data/level_part_node.json';
      } else { // d3 event
        event = d3.event;
        url = 'data/level_part_link.json';
      }
      var node = this.nodeInfo;
      //alert(node.name);
      $.ajax({
        url: url,
        dataType: "json",
        success: function(flowData) {
          allToPart(flowData);
        },
        error: function() {}
      });
      event.stopPropagation();
    };

    d3.json("data/level.json", function(flowData) {
      //init
      init();

      //layout
      sankey
        .nodes(flowData.nodes)
        .links(flowData.links)
        .layout(0);

      //save data
      flowData.nodes.forEach(function(d, i) {
        d.indexInArray = i;
      });
      flowData.links.forEach(function(d) {
        data.linkIndex[d.source.indexInArray + '_' + d.target.indexInArray] = d;
      });
      data.nodes = flowData.nodes;
      data.links = flowData.links;

      //draw
      createHead(flowData.nodes);
      createSVG(flowData);
      createJqNodes(flowData);

      //bind event
      //delegate rect click and hover event to chart
      $("#chart").delegate("div.rect", "click", allClickListener);

      $("#chart").delegate("div.rect", "mouseover", function() {
        var node = this.nodeInfo;
        if (node.level === 0) {
          return;
        }
        //set floatTag content
        floatTag.html('<div><p>' + node.name + '</p>' + '<p>' + node.output + '位点击通过(' + d3.format(",.2f")(node.output / node.input * 100) + '%)</p>' + '<p>' + (node.input - node.output) + '位访问者流失(' + d3.format(",.2f")((1 - node.output /
          node.input) * 100) + '%)</p>' + '<p>' + node.input + '访问次数</p>' + '</div>');
        floatTag.css({
          "visibility": "visible"
        });
      });

      $("#chart").delegate("div.rect", "mouseout", function() {
        var node = this.nodeInfo;
        if (node.level === 0) {
          return;
        }
        floatTag.css({
          "visibility": "hidden"
        });
      });

      //click listener in "part" state;
      $("#chart").on("click", function() {
        if (state === "part") {
          partToAll();
        }
      });
    });
  </script>
</body>

</html>
